nkImages : Decoding Images

Decoding images within nkImages is very simple, and allows for customizations on the memory layout and output format. For this tutorial, we will decode a Dds image and see what we can do with it. Reference image is a 512x512 gray scale cloud map :

Let's see what we will be able to do from there !

Decoding an image

To decode an image, it is necessary to use the dedicated Encoder. For Dds images, the DdsEncoder can be used.

Let's start with including the basics to use the component with the encoder we want :

#include <NilkinsImages/Data/AlignmentDescriptor.h> #include <NilkinsImages/Data/Image.h> #include <NilkinsImages/Dds/DdsEncoder.h>

While we could use standard library with vectors containing the loaded data, for demonstration purpose, we will use other parts of the engine to showcase how it fits together. As such, we will also need headers from nkResources, nkMemory and nkMaths.

// nkMaths #include <NilkinsMaths/Algebra/Vector.h> // nkMemory #include <NilkinsMemory/Containers/Buffer.h> // nkResources #include <NilkinsResources/ResourceManager.h>

Now that we are ready to start, we can ask to load our image into memory :

nkMemory::Buffer imgData = nkResources::ResourceManager::getInstance()->loadFileIntoMemory("Data/cloudMap.dds") ;

Using the nkResources::ResourceManager, we can easily get the file into a nkMemory::Buffer that we can transmit to nkImages. The dedicated encoder can take it as-is :

if (!nkImages::DdsEncoder::canDecode(imgData)) return 0 ; nkImages::Image img = nkImages::DdsEncoder::decode(imgData) ;

Ensuring the encoder can parse the data by first calling the JpgEncoder::canDecode method can help in detecting if a file is the format we want. Either way, if the encoder cannot parse given data, it will return an empty image when its JpgEncoder::decode method is called.

What we get from it is a ready-to-use Image. Let's try and see what are the dimensions of parsed image :

std::cout << "Dimensions : " << img.getWidth() << "x" << img.getHeight() << std::endl ;
Dimensions : 512x512

We can also query a specific pixel of the image :

nkMaths::Vector pixel = img.getPixel(16, 16) ; std::cout << "Pixel at (16, 16) : " << pixel._x << ", " << pixel._y << ", " << pixel._y << std::endl ;
Pixel at (16, 16) : 58, 58, 58

Using it within the engine

An Image can be used directly into the engine. Current good use-cases are within nkGraphics and nkWinUi.

Within nkWinUi

Images can be used within the interface. For instance, it can be used as an icon for a Window :

window->setIcon(img) ;

Doing so will alter the system bar of the window :

The icon will also be present in the task bar for attached window.

Within nkGraphics

An Image can be used within a Texture during rendering :

tex->setFromImage(img) ;

After loading, mapping the texture and finding back the pixel's memory spot, we get :

Texture pixel at (16, 16) : 58, 58, 58

Which is the same value as in the image, as expected !

Baking in alignment or format constraints

To get this image loaded within the texture, we had to fulfill some constraints. This image stores BGR values, each on one byte. However, for 1 byte per channel formats, a texture requires pixels to be encoded over 4 bytes.

BGR having only 3 bytes, we need to add one byte of padding. We have two choices :

Here, we will choose to add the alpha format, as this will ensure the texture is working fine even when blending is enabled. Changing the alignment means that the bonus byte might get a value not suitable for such cases.

To alter how an image is parsed, we need the AlignmentDescriptor :

nkImages::AlignmentDescriptor alignmentDesc ; alignmentDesc._alphaMode = nkImages::ALPHA_MODE::ALPHA ; nkImages::Image img = nkImages::DdsEncoder::decode(imgData, alignmentDesc) ;

To request an alpha channel, we change the alpha mode. Then, by communicating this when decoding the image, we get an image with the alpha channel defaulted to opaque, precisely what we needed.

This descriptor structure can be used to alter memory alignment and format constraints. It is usable by all encoders, the same way as demonstrated.

Accessing and changing data

Reading images can be useful, but sometimes we also want to work on it. It is possible to query the decompressed data memory, and work directly on it for any purpose. For safety, we can query all memory alignment values to be sure we're accessing the data in the right fashion.

Let's alter the image to nullify the blue channel. We will need to loop into its pixel memory, setting its first channel to 0, as it is a BGR image. For that, we first need to gather our variables :

unsigned char* dataPtr = img.getDataPtr() ; unsigned int width = img.getWidth() ; unsigned int height = img.getHeight() ; unsigned int lineStride = img.getRowByteSize() ; unsigned int pixelStride = img.getPixelByteSize() ;

To loop over all pixels, we will need to know what the size of one pixel is, and then what the size of one row is. While not necessary in this simple case as we know our format and alignment, in more general case this will enable us to get expected results. They can easily be used in a loop :

for (unsigned int y = 0 ; y < height ; ++y) { unsigned int lineOffset = y * lineStride ; for (unsigned int x = 0 ; x < width ; ++x) { unsigned int pixelOffset = lineOffset + x * pixelStride ; dataPtr[pixelOffset] = 0 ; } }

We simply loop over all rows, and then on their pixels, computing the right offset in memory to access them. First channel will be at the start of pixel's memory spot, so we set it to 0, effectively nullifying the blue channel. If we now reencode the image using the encoder and output it to a file, we can easily see the result :

Our image turned yellow, now that it is only composed of red and green channels ! This shows that data can be easily and safely altered within the image.

Tools

Next to that, there are some other tools that can be used within the component.

CompositeEncoder

While in this case we knew the data format upfront, this is not always the case. And while motivated to choose the right encoder, maybe we won't be each time.

In such cases, the CompositeEncoder is provided. Its purpose is to centralize all other encoders and choose the right one depending on which one can decode provided data. It can be used like any other encoder.

ConversionUtils

Format conversions can be required for many purposes. Fitting an image into a texture, applying algorithms requiring YUV or RGB...

The ConversionUtils is the class to use if such need arises. It can convert either one pixel's value, or a full image right away, from a format to another.